Repository navigation
Conversation
A contract token can have up to 18 decimals (the backend reads decimals() on chain); the swap flow assumed 7 everywhere. - decodeSwapTx accepts contract tokens (a contract token is its own contract ID) and reads the amount sold at the sold token's decimals. A classic path payment for a contract token is refused. - getMinOut rounds to the bought token's decimals: the backend parses minOut at that precision and rejects extra digits. - getSwapTokenDecimals: 7 for classic; a contract token's from the held balance, else from the picked row. The build refuses when a contract destination's decimals are unknown (only reachable via a hand-typed destination_asset URL for a token neither held nor picked). - The sell input, its validation and the build use the source's decimals from balances, no longer via isToken, which a later destination pick overwrites. - Custom networks: a contract token has no route; Horizon isn't asked. - The "custom tokens not supported" error is gone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Spec §6: the trustline, its 0.5 XLM reserve and the reserve sheet are classic-only. A non-held contract destination counted as needing a trustline (two-op fee, reserve preflight, Max less 0.5 XLM). A held contract source was offered as the reserve sheet's sell token, whose receive quote is classic-only; it still has to cover the fee in XLM. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Spec §6 / C3: the picker keeps its sources (held tokens, stellar.expert search and trending, asset lists) and stops filtering out Soroban contract tokens. - Held contract tokens appear in "Your tokens" (both pickers), with their balance at their decimals. - Search and trending keep contract records, carrying stellar.expert's decimals onto the picked token, so the build knows them. - Contract tokens are bulk-scanned with Blockaid as CODE-CONTRACT, as the Add token flow scans them. - A contract token needs no trustline. isContract now means a Soroban- only token (contract issuer); a SAC is listed in its classic form. - The "Soroban tokens aren't supported" empty state, hadSorobanMatches and the picker's now-unused isDestination prop are gone; e2e test 5 now expects the contract token's row. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Spec §6 row subtitle: classic tokens keep the issuer's home domain; a Soroban-only token shows its contract ID shortened (CBI7…IHRV), even if it reports a domain, so a lookalike still looks different. Held rows keep the balance subtitle they share with the account home. The row menu's "View on stellar.expert" opens a contract token's contract page; /asset/CODE-C… is not a stellar.expert page. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…hed on pick Spec §6: icons come from the user's asset lists, then the issuer's stellar.toml; the first hit is cached so the token looks the same across the wallet. Discover rows showed stellar.expert's toml image even when a list (or the wallet's icon cache) had another. Rows now take the wallet's cached icon, else the first asset list's, else the toml image stellar.expert returned with the record (its copy of the issuer's stellar.toml, so no extra Horizon + toml round trip per row). The picked token's icon is written to the Redux and background icon caches, which the amount screen, review and account home read. Rows are not cached at search time, so searches don't fill the cache with tokens nobody picked. findTokenListIcon is getIconFromTokenLists' lookup without its cache write, so the picker can run it on every row. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
User-approved decision: a contract token the account doesn't hold gets its decimals from getDecimalsForAsset (the existing on-chain read via /token-details), not from the stellar.expert row it was picked from and never a silent 7. The read runs at the start of the build; if it fails, the build stops before any quote or /swap/tx call, shows "Couldn't read this token's details", and reports to Sentry. getSwapTokenDecimals is gone; getHeldTokenDecimals only reads a held balance, and getDecimalsForAsset is the one resolver. The sell input still uses 7 for an unheld source, which has no balance to swap. New tests: on-chain decimals reach /swap/tx's minOut; a 6 -> 18 decimal build scales the amount sold and minOut to each side. Co-Authored-By: Claude Opus 5.5 <[email protected]>
swapQuote's contractIssuer (StrKey.isValidContract) and decodeSwapTx's own StrKey check duplicated isContractId; both now use it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
TX_SEND_MAX (922337203685.4775807) is the int64 stroop limit of a classic amount; a contract token's i128 balance has no such cap, and at 18 decimals the cap is not even the right number. Send's validator has the same cap and 7-decimal check for tokens; left alone here. Co-Authored-By: Claude Opus 5.5 <[email protected]>
User-approved decision: the picker shows icons through its lookup (the wallet's icon cache, then asset lists, then the toml image) but never writes the shared icon cache. Drops the pick-time write from 98dbc66; the lookup and the shared icon pipeline are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
User-approved decision: only mainnet has a source (LI.FI) that routes contract tokens, so on testnet and other networks search and Popular show no contract token the account doesn't hold. Held ones stay in Your tokens. buildSwapSections now reads the networkDetails it was passed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
User-approved decision: the asset details screen's Swap button now shows for contract tokens too (not for liquidity-pool shares), opening Swap with the token as the source, now that the swap flow handles them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
A SAC added by contract ID without a trustline is held Soroban-shaped (injectLocalTokenBalances), named after its classic asset. Picked as the destination it counted as a contract token, so no reserve preflight and no 0.5 XLM held back from Max before review. Checked against freighter-backend-v2 #178 (lifi.go toClassic/BuildTx): the backend detects a SAC destination from its contract instance and lets LI.FI's trust(account) through; decodeSwapTx reads it as addsTrustline, so the build and the review's trustline banner already worked. Only the pre-review reserve check was missing. An unheld SAC returned by stellar.expert as a bare contract record would still be missed; stellar.expert returns SACs in classic form. Co-Authored-By: Claude Opus 5.5 <[email protected]>
"Swap for 0.5 XLM" swaps the best classic balance into XLM, but left isToken as a Soroban source had set it, so the review showed empty Soroban fee rows and asked RPC whether the classic source's SAC supports muxed addresses. Both sides are classic after it. The direction toggle also writes isToken false when the new source is a held Soroban token; the swap review reads Soroban fees from the build's resource fee, so that one shows nothing wrong and is left as is. Co-Authored-By: Claude Opus 5.5 <[email protected]>
stellar.expert can return a SEP-41 contract with no code metadata; the picker showed it as a blank row and carried an empty code to the amount card and review. Its code falls back to the shortened contract ID, as its subtitle already shows. The Add token flow's mapStellarExpertRecord has the same empty fallback; outside swap, left as is. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Contributor
|
PR Preview build is ready: https://github.com/stellar/freighter/releases/tag/untagged-b9a990eb32cef1474b1e |
…n-tokens master's #3065 routes asset search and Popular tokens through backend v2 from the service worker. Resolutions: - trendingAssets.ts: master's getStellarExpertAssets fetch, keeping this branch's contract-token mapping (truncateString, isContractId). - trendingAssets.test.ts: master's respondWithRecords message mock, widened to code/decimals; this branch's two contract-token tests moved onto it from the fetch spy. - swap.test.ts: the contract-address search test stubs ASSET_SEARCH_ROUTE on the context with this branch's TKN record; the page-level stellar.expert route no longer intercepts anything. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings in the merge of 6f50255 (quote loading state; Review gated until the quote for the current inputs lands) into feat/swap-backend-tx. Conflicts: none textual. Semantic fix: getSwapDerivedData now takes publicKey and swapQuoteKey (to tell whether the stored quote matches the inputs). The "getSwapDerivedData with a contract token sold" tests in SwapAmount.sorobanTokens.test.tsx call it directly, so they now pass a key matching their inputs (the quote has landed); their assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts: # extension/src/popup/components/swap/SwapAmount/index.tsx
# Conflicts: # extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.liveQuote.test.tsx # extension/src/popup/components/swap/SwapAmount/hooks/__tests__/useSimulateSwapData.test.ts # extension/src/popup/components/swap/SwapAmount/hooks/useSimulateSwapData.tsx # extension/src/popup/helpers/swapQuote.ts
piyalbasu
marked this pull request as ready for review
November 11, 2026 22:38
A held contract token is keyed by its on-chain symbol (SYM:C…) and a search or Popular row for the same contract by stellar.expert's code (ABC:C…), so every identity check saw two tokens: the row listed as new next to the held balance, a hidden token resurfaced in search, picking the held contract as destination did not reset the source, and the direction toggle treated the held destination as non-held. Mobile keys Soroban tokens on the contract ID (canonicalId); the extension now does the same through swapIdentityKey at each comparison, leaving stored canonicals and the wire format untouched. Sites changed (gate 1 grep of canonical-vs-canonical comparisons under src/popup/components/swap and src/popup/views/Swap): buildSwapSections held set, verified set, Popular filter and search dedupe; omitHiddenFromSections; SwapPickerSections hiddenAssets; withRowIcons cached-icon lookup; getSwapDerivedData.destinationIsNonHeld; the Swap view's two same-token resets. Not changed: tokenPrices / icons maps read by canonical (storage keys, not identity), getHeldTokenDecimals and findAssetBalance (already by contract). Search dedupe now excludes every held identity from the discover sections, not only the held rows matching the term, so a held contract never lists as new under a code the account does not use for it. Found by /code-review on #3073. Co-Authored-By: Claude Fable 5.1 <[email protected]>
The pick committed the token at once and the build read its decimals later, so a user could type an amount and see a live quote before "Couldn't read this token's details" on Review, the receive card sat at 7 decimals meanwhile, and swap.destination_selected counted picks that never became quotable. Mobile reads at pick and refuses the pick; the extension now does the same (Piyal's call, 2026-11-11). The destination picker reads getDecimalsForAsset (the backend's token-details on public networks) before applying an unheld contract pick, stores the result on the selection, and refuses the pick with an error notice when the read fails (reported to Sentry, no metric). A repeat tap while a read is pending is ignored, and every pick supersedes a pending read so a late result cannot apply. The picked row no longer carries stellar.expert's toml-sourced decimals. The build uses the stored decimals and keeps the build-time read only for a destination that never went through the picker (defaulted or deep-linked), which still fails closed. The receive card's cryptoDecimals was dead: AmountCard reads it only in the input onChange, which the read-only card stubs. The prop is now optional (default 7) and dropped from that card. Found by /code-review and the drift review on #3073. Co-Authored-By: Claude Fable 5.1 <[email protected]>
The picker sent every search term to stellar.expert, so a contract token it does not index, or any term during a stellar.expert outage, came back "No tokens match" and could not be picked, though quoting and building would handle it. Mobile reads a pasted contract ID through token-details (handleContractLookup); the extension now does the same (Piyal's call, 2026-11-11), reusing the Add-token lookup's pieces: the backend's token details, isSacContractExecutable for a SAC, which is listed as its classic asset so the trustline logic applies, and the native row for XLM's own contract. getTokenDetails gains throwOnFailure, as mobile's did, so a failed read can be told apart from "not a token": a 400 is an answer (null), any other failure throws into the lookup's existing catch, which serves the cached result or the held-only fallback with its "discovery unavailable" notice, the same path a failed stellar.expert search takes. Other callers keep the old swallow-and-null behaviour. Found by /code-review and the drift review on #3073. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Active moderate and critical findings remain in the live-quote preflight, Soroban asset handling, picker identity/display logic, and multi-hop route normalization.
5 open issues
What changed in this PR
Adds Soroban SEP-41 tokens to the swap picker and transaction flow alongside classic assets, with contract-token discovery, decimal-aware quoting and transaction handling, trustline behavior, icons, and asset-detail integration.
Changes:
- Supports Soroban tokens in balances, search, Popular tokens, and pasted contract-ID flows with mainnet filtering and contract-ID subtitles.
- Propagates token-specific decimals through validation, quoting, transaction construction, review decoding, and minimum-received calculations.
- Updates trustline handling, Stellar Expert links, localization, icon lookup, unit tests, and swap E2E coverage.
| File | Description |
|---|---|
| extension/src/popup/views/__tests__/Swap.telemetry.test.tsx | Updates swap telemetry fixtures for the revised token lookup result. |
| extension/src/popup/views/__tests__/Swap.selectionType.test.tsx | Updates selection fixtures for the revised swap picker result shape. |
| extension/src/popup/locales/pt/translation.json | Adds Portuguese token-detail error text and removes obsolete Soroban swap copy. |
| extension/src/popup/locales/en/translation.json | Adds English token-detail error text and removes obsolete Soroban swap copy. |
| extension/src/popup/locales/__tests__/translationParity.test.ts | Updates the expected swap translation parity keys. |
| extension/src/popup/helpers/trendingAssets.ts | Preserves Soroban contract-token records and metadata from trending assets. |
| extension/src/popup/helpers/swapQuote.ts | Converts contract assets for backend routing and exposes held-token decimals. |
| extension/src/popup/helpers/decodeSwapTx.ts | Decodes classic and Soroban swaps using token-specific decimal handling. |
| extension/src/popup/helpers/__tests__/trendingAssets.test.ts | Tests trending Soroban records, decimals, and fallback naming. |
| extension/src/popup/helpers/__tests__/swapQuote.test.ts | Tests contract-token routing and held-token decimal lookup. |
| extension/src/popup/helpers/__tests__/decodeSwapTx.test.ts | Tests Soroban transaction decoding and decimal handling. |
| extension/src/popup/ducks/transactionSubmission.ts | Makes destination-token decimals optional for unresolved contract tokens. |
| extension/src/popup/components/swap/SwapAsset/SwapTokenMenu/index.tsx | Links contract tokens to their corresponding Stellar Expert pages. |
| extension/src/popup/components/swap/SwapAsset/SwapTokenMenu/__tests__/SwapTokenMenu.test.tsx | Tests classic and contract-token Stellar Expert links. |
| extension/src/popup/components/swap/SwapAsset/SwapPickerSections/index.tsx | Displays contract IDs and token metadata in picker sections. |
| extension/src/popup/components/swap/SwapAsset/SwapPickerSections/__tests__/SwapPickerSections.test.tsx | Tests contract-ID subtitles and updated picker empty states. |
| extension/src/popup/components/swap/SwapAsset/index.tsx | Removes obsolete Soroban unsupported-state wiring from the asset picker. |
| extension/src/popup/components/swap/SwapAsset/hooks/useSwapTokenLookup.ts | Includes Soroban balances and discovery records, applies network filtering, and resolves icons. |
| extension/src/popup/components/swap/SwapAsset/hooks/__tests__/useSwapTokenLookup.test.ts | Tests held, popular, searched, and off-mainnet Soroban token behavior. |
| extension/src/popup/components/swap/SwapAsset/hooks/__tests__/useSwapTokenLookup.soroban.test.tsx | Tests Soroban search results, decimals, icons, and Blockaid integration. |
| extension/src/popup/components/swap/SwapAsset/hooks/__tests__/useSwapTokenLookup.abort.test.tsx | Updates lookup cache fixtures with icon state. |
| extension/src/popup/components/swap/SwapAsset/__tests__/SwapAsset.test.tsx | Updates asset-picker fixtures for the revised Soroban lookup behavior. |
| extension/src/popup/components/swap/SwapAmount/index.tsx | Applies token decimals and SAC-specific trustline logic to swap amounts. |
| extension/src/popup/components/swap/SwapAmount/hooks/useSimulateSwapData.tsx | Resolves missing decimals, scales minimum output, and reports decimal lookup failures. |
| extension/src/popup/components/swap/SwapAmount/hooks/__tests__/useSimulateSwapData.test.ts | Tests decimal-aware minimum-output calculations and error mapping. |
| extension/src/popup/components/swap/SwapAmount/hooks/__tests__/useSimulateSwapData.quote.test.tsx | Tests Soroban quote building, decimal scaling, and decimal-read failures. |
| extension/src/popup/components/swap/SwapAmount/hooks/__tests__/useSimulateSwapData.i18n.test.tsx | Updates simulation fixtures with token decimal metadata. |
| extension/src/popup/components/swap/SwapAmount/helpers/swapAmountValidation.ts | Validates precision per token and applies send caps only to classic sources. |
| extension/src/popup/components/swap/SwapAmount/helpers/getSwapDerivedData.ts | Updates reserve and fee derivation for Soroban token sources. |
| extension/src/popup/components/swap/SwapAmount/helpers/__tests__/swapAmountHelpers.test.ts | Tests token-specific precision and classic-versus-contract amount limits. |
| extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.sorobanTokens.test.tsx | Tests Soroban decimal propagation, trustlines, reserve behavior, and fees. |
| extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.reserveQuote.test.tsx | Tests swap-state reset behavior when replacing a Soroban source. |
| extension/src/popup/components/swap/SwapAmount/__tests__/SwapAmount.liveQuote.test.tsx | Tests live quote behavior for Soroban destinations. |
| extension/src/popup/components/InternalTransaction/ReviewTransaction/index.tsx | Allows unresolved destination-token decimals in review metadata. |
| extension/src/popup/components/account/AssetDetail/index.tsx | Enables Swap for non-liquidity-pool Soroban assets. |
| extension/src/popup/components/__tests__/AssetDetail.test.tsx | Updates asset-detail expectations for the Soroban Swap button. |
| extension/e2e-tests/swap.test.ts | Replaces the unsupported-token scenario with Soroban contract-token discovery coverage. |
| @shared/api/types/types.ts | Adds optional decimals to shared trending-asset records. |
| @shared/api/helpers/getIconFromTokenList.ts | Adds side-effect-free token-list icon lookup for swap picker rows. |
| @shared/api/helpers/__tests__/getIconFromTokenList.test.ts | Tests token-list icon matching and priority behavior. |
Review effort: Lite
edab953 excluded every held identity from the search results. A held contract searched by stellar.expert's code (not its balance's symbol) then vanished: the held row did not match the term, and the search row was dropped as held. Mobile dedupes search rows only against the held rows the term matched and flags the rest held by contract ID; the extension now does the same. Popular still excludes every held token, as mobile's popularTokens does. Co-Authored-By: Claude Fable 5.1 <[email protected]>
The toggle cleared isToken unconditionally. With C3 a held contract token can sit on either side, and the review reads isToken against srcAsset (getContractIdFromTransactionData) for its contract memo and muxed-address checks, so toggling a held contract token into the sell slot left the review treating it as classic. isToken now follows the token that lands in the sell slot; an empty slot (a non-held destination that could not move) reads as classic. Every saveIsToken in the swap flow, and which side it derives from: - SwapAmount reserve prefill: false; the new source is a classic non-XLM token (correct). - SwapAmount direction toggle: now the new source (this commit). - Swap view source pick: the picked source's isContract (correct). - Swap view destination pick: the picked DESTINATION's isContract, which is the other side from what the review reads; left as is here. - Swap view deep-link source: false only when the source param is XLM; a classic or contract source param leaves the previous value; left as is here. Copilot thread on #3073. Co-Authored-By: Claude Fable 5.1 <[email protected]>
handleDestinationPick decided "held" from the Your tokens rows, which in search mode are only the rows the term matched. A held SYM:C1 found through its ABC:C1 search row (listed as held since 44e8fee) then got a pick-time decimals read and was stored as ABC:C1. The pick now looks the row's contract up against every held balance, the set Popular filters against, and a held match is picked as the balance: its canonical, symbol and decimals, no read. Mobile's picker takes the same rule. Medium review on the C3 fix round. Co-Authored-By: Claude Opus 5.5 <[email protected]>
isToken describes the source: the review reads it against srcAsset for its contract memo and muxed-address checks (see 0912ca0). The destination pick set it from the destination, so a contract source with a classic destination picked was reviewed as classic, and a contract destination flipped it on for a classic source. The pick no longer writes it; when it clears the source (same-token reset) it clears isToken with it. Medium review on the C3 fix round. Co-Authored-By: Claude Opus 5.5 <[email protected]>
getTokenDetails parsed the body before checking for the 400 that means
"not a token", so the backend's plain-text 400 ("Soroban has been
disabled on pubnet") threw a SyntaxError, which throwOnFailure turns
into a failed lookup. The status is now checked first, matching
mobile's axios path, which never parses a 400's body.
Medium review on the C3 fix round.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The read took the public key from the picker's account data, which can still be loading when Popular rows are already on screen; a tap then sent an empty key and the backend answered 400. It now reads the active account's key from Redux (publicKeySelector), as the rest of the swap flow does. Medium review on the C3 fix round. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


What
The swap token picker now offers Soroban contract tokens (SEP-41) alongside classic assets. You can find them through search, Popular tokens, a pasted contract ID (looked up through the backend, so tokens stellar.expert doesn't index still resolve), or your balances, and swap them through LI.FI on mainnet. The Swap button on a Soroban token's details screen now works too.
CBI7…IHRV), so a lookalike still looks different.Draft, for diff review. Stacked on C2 (
feat/swap-backend-tx, which is stacked on C1,feat/swap-backend-quotes): C2 is #3075, C1 is #3074. Needs stellar/freighter-backend-v2#178 live first.Why
Swap routing spec, client item C3: stellar/wallet-eng-monorepo#131.
Implementation details (for agents)
What changed
useSwapTokenLookup,buildSwapSections,trendingAssets): Soroban records are no longer filtered out. A contract token's identity is its contract ID (swapIdentityKey) for dedupe, hidden filtering and the same-token reset; a held token picked under another code is picked as the balance. A pasted contract ID goes to the backend token lookup, not stellar.expert. Rows carry the shortened contract ID as their subtitle. A record with no code is named by its contract ID.getDecimalsForAsset) and stored with the selection; a failed read refuses the pick and goes to Sentry. The build uses the held or stored decimals and reads only for a destination that never went through the picker.isTokenfollows the sell-side token (the review reads it against the source): the direction toggle sets it, the destination pick no longer does.validateSwapAmounttakesisClassic, so the classicTX_SEND_MAXcap applies to classic sources only.isSacContractidentifies as a SAC, the trustline is checked against its classic canonical.isContractId) replaces three.Verification
/code-reviewat high, then medium on each fix round.Known follow-ups
/token-detailsanswers 400 for every failure, so a lookup outage reads as "not a token" and a pasted ID shows "No tokens match" instead of the unavailable notice (both platforms). Needs 404 for not-a-token and 5xx for upstream failures.sourceanalytics value is "popular" here and "trending" on mobile.Screenshots
Mainnet, e2e build with stubbed backend responses. Left: pasting a contract ID lists the Soroban token with its shortened contract ID. Right: an XLM → Soroban token quote through a LI.FI route, at the token's 18 decimals. The mobile PR (stellar/freighter-mobile#1048) shows the same flow against a real backend, through review.
🤖 Generated with Claude Code